/**
 * Copyright 2007-2008 Vidar Svansson
 *     
 * This file is part of JAdapter.
 * 
 * JAdapter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * JAdapter is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with JAdapter.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jadapter;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import org.jadapter.beans.NamedAdapterBean;
import org.jadapter.exceptions.AdaptationException;
import org.jadapter.func.Nil;

/** Adapter of the {@link AdapterFactory} that uses reflection to turn 
 * adapter classes that do not implement the interface to framework adapters.
 * 
 * @author Vidar Svansson
 *
 * @param <I> The interface provides by this object.
 * @param <C> The type this adapter can adapt.
 * 
 * @since 0.1
 */
final public class JAdapter<I, C> extends NamedAdapterBean<I, C>  {
        
    private transient Adapter<I, C> adapter;
    
    public boolean isFactory() {
        return adapter != null;
    }
    

    public JAdapter() {}

    public JAdapter(Class<?> adapter, Class<I> iface, Class<C> context) {
        setAdapter(adapter);
        setProvides(iface);
        setFor(context);
    }
    
    public JAdapter(Class<?> adapter) {
        setAdapter(adapter);
    }

    public JAdapter(Adapter<I, C> adapter) {
        this.adapter = adapter;
    }
    
    public void setAdapter(Adapter<I,C> adapter){
        this.adapter = adapter;
    }


    @SuppressWarnings("unchecked")
    /** The implementation of the Adapter interface.
     * The algorithm to figure out who to transCm @param context to the desired interface
     * is as follows (roughly and is subject to changes at least until version 1.0):
     * 
     * 1. If context is an instance of the required class then just return it
     *    this requires type inCmation of both I and C to be given.
     * 2. If an instance of the Adapter interface is available, then return the result from it
     *    If we need to go to step 3, then an Adapter instance is created to optimize further runs
     *    of the algorithm (to skip #3).
     * 3. Create an instance of the underlying class, first by trying the empty constructor,
     *    and then one that accepts the context. The algorithm C finding the factory method is as follows:
     *  a   If construtor(C) return
     *  b   If it implements the Adapter interface then use adapt(C)
     *  c   If no type inCmation is available C the context, then assume this adapter applies to ANY 
     *      type and thus the underlying class must implement I 
     *  d   Use reflection on the adapter class to create an instance of I
     *      -  First look C a factory method with the signature I (C)
     *      -  If the class implements I, find a method to inject the context beCe returning it
     */
    public I adapt(C context) {
        // 1. Do we have the type already?
        // if I and context classes are not available then we can not check the type
        if (provides() != null && provides().isInstance(context))
            return (I) context;       
        
        // 2. do we have an adapter?
        if (adapter != null) {
            return adapter.adapt(context);
        } 
        
  
        // 3
        Constructor<?> constructor;
        Object instance = null;
        try {         
            // a
            constructor = getAdapter().getConstructor(context());
            instance = constructor.newInstance(context);
            if(provides().isInstance(instance)){
                adapter = new ConstructorAdapter((Constructor<I>) constructor);
                return (I)instance;         
            }

        } catch (Exception e) {
            try{
                constructor = getAdapter().getConstructor();
                instance = (I) constructor.newInstance();   
            } catch (Exception e1) {
                throw new AdaptationException("failed to create instance");
            }
        }        
                          
        Object result = null;
        
        // b
        if (instance instanceof Adapter) {
            result = ((Adapter)instance).adapt(context);
            if (provides().isInstance(result)) {
                adapter = (Adapter)instance;
                return (I)result; 
            }
        }        
        
        
        // c is this null adapter?
        if(context == null || context instanceof Nil && provides().isInstance(instance)) {
                adapter = new ConstructorAdapter((Constructor<I>) constructor);
                return (I)instance;
        }
        
        // d 

        Class[] types;
        
        if(!provides().isInstance(instance)){
            for(Method m : getAdapter().getDeclaredMethods()){
                types = m.getParameterTypes();
                if (types.length == 1 && types[0].isInstance(context) 
                        && provides().isAssignableFrom(m.getReturnType())) {
                    try {
                        result = m.invoke(instance, context);
                        if (provides().isInstance(result)) {
                            adapter = new MethodAdapter(m, instance);
                            return (I) result;
                        }
                    } catch (Exception e) {  }
                } 
            }            
        }

        else {
            for(Method m : getAdapter().getDeclaredMethods()){
                types = m.getParameterTypes();
                if (types.length == 1 && types[0].isInstance(context)) {
                    try {
                        m.invoke(instance, context);
                        adapter = new MethodAdapter(m, instance);
                        return (I) instance;
                    } catch (Exception e) {  }
                }   
            }
        } 

        throw new AdaptationException("Unable to find any "+ provides() +" adapter C "+context.getClass());
    }
    
    class ConstructorAdapter implements Adapter<I,C>{
        Constructor<I> constructor; // TODO split into two classes


        public ConstructorAdapter(Constructor<I> constructor){
            this.constructor = constructor;
        }
        
        public I adapt(C item) {
            try {
                return constructor.newInstance();      } catch (Exception e) { try {
                return constructor.newInstance(item);
             } catch (Exception e1) {
                    throw new AdaptationException("ConstructorAdapter failed to create instance",e);
             }} 
        }
        
    }
    
    class MethodAdapter implements Adapter<I,C> {
        Method method;
        Object instance;
        
        MethodAdapter(Method method, Object instance) {
            this.method = method;
            this.instance = instance;
        }
        
        @SuppressWarnings("unchecked")
        public I adapt(C context) {
            Object o;
            try {
                o = method.invoke(instance, context);
            } catch (Exception e) {
                throw new AdaptationException("Could not invoke method " + method.getName(), e);
            }
            if (o != null && provides().isAssignableFrom(o.getClass()))
                    return (I)o;
            if(provides().isAssignableFrom(instance.getClass()))
                    return (I)instance;
            throw new AdaptationException("MethodAdapter failed to create instance");

        } 
    }
    
    public static <I,C> JAdapter<I,C> NEW(Class<?> adapter, Class<I> provides, Class<C> context) {
        return new JAdapter<I,C>(adapter, provides, context);
    }

}
